package de.milo.map;

import org.newdawn.slick.Renderable;

import de.milo.map.tile.MapTile;

/**
 * This class renders maps.
 * @author Mizi
 *
 */
public class MapViewer implements Renderable
{
	private Map map;
	private Map viewableMap;
	private ViewportInteger viewport;
	private int tileWidth;
	private int tileHeight;
	private int offsetLeft;
	private int offsetTop;
	private int offsetRight;
	private int offsetBottom;
	
	/**
	 * Creates a new MapViewer instance that shows the given map.
	 * Each tile of this MapViewer instance is samesized, so
	 * each mapttile used by the given map should also have the
	 * given tilewidth and tileheight.
	 * @param map the map shall be shown by this MapViewer instance
	 * @param tileWidth the width of the maptiles in pixels
	 * @param tileHeight the height of the maptiles in pixels
	 */
	public MapViewer(Map map, int tileWidth, int tileHeight)
	{
		this.map = map;
		this.viewport = new ViewportInteger();
		this.tileWidth = tileWidth;
		this.tileHeight = tileHeight;
		updateViewableMap();
	}
	
	/**
	 * Returns the tile on the given location of the map.
	 * @param x the x-position of the tile
	 * @param y the y-position of the tile
	 * @param layer the layer this tile belongs to
	 * @return the tile on the given location
	 */
	public MapTile getMapTile(int x, int y, int layer)
	{
		return map.getTile(x, y, layer);
	}
	
	/**
	 * Returns the width of a tile.
	 * @return the width of a tile in pixels
	 */
	public int getTileWidth()
	{
		return tileWidth;
	}
	
	/**
	 * Returns the height of a tile.
	 * @return the height of a tile in pixels
	 */
	public int getTileHeight()
	{
		return tileHeight;
	}
	
	/**
	 * Returns the width of the shown map in tiles.
	 * @return the width of the shown map in tiles
	 */
	public int getMapTileWidth()
	{
		return map.getWidth();
	}
	
	/**
	 * Returns the height of the shown map in tiles.
	 * @return the height of the shown map in tiles
	 */
	public int getMapTileHeight()
	{
		return map.getHeight();
	}
	
	/**
	 * Returns the width of the shown map in pixels.
	 * @return the width of the shown map in pixels
	 */
	public int getMapPixelWidth()
	{
		return map.getWidth() * tileWidth;
	}
	
	/**
	 * Returns the height of the shown map in pixels.
	 * @return the height of the shown map in pixels
	 */
	public int getMapPixelHeight()
	{
		return map.getHeight() * tileHeight;
	}
	
	/**
	 * Returns the x-position of the viewport.
	 * @return x-position
	 */
	public int getViewportX()
	{
		return viewport.getX();
	}
	
	/**
	 * Returns the y-position of the viewport.
	 * @return y-position
	 */
	public int getViewportY()
	{
		return viewport.getY();
	}
	
	/**
	 * Returns the width of the viewport.
	 * @return the width of the viewport
	 */
	public int getViewportWidth()
	{
		return viewport.getWidth();
	}
	
	/**
	 * Returns the height of the viewport.
	 * @return the height of the viewport
	 */
	public int getViewportHeight()
	{
		return viewport.getHeight();
	}
	
	/**
	 * Change the shown map to a new map.
	 * @param map the new map that shall be shown
	 */
	public void setMap(Map map)
	{
		this.map = map;
		updateViewableMap();
	}
	
	/**
	 * Sets the tile on the given location of the map.
	 * @param x the x-position of the tile
	 * @param y the y-position of the tile
	 * @param layer the layer this tile belongs to
	 * @param tile the tile to set
	 */
	public void setMapTile(int x, int y, int layer, MapTile tile)
	{
		map.setTile(x, y, layer, tile);
	}
	
	/**
	 * Sets the visible area of this MapViewer instance in pixels.
	 * @param x the x-position of the visible area on the shown map
	 * @param y the y-position of the visible area on the shown map
	 * @param width the width of the visible area
	 * @param height the height of the visible area
	 */
	public void setViewportInPixels(int x, int y, int width, int height)
	{
		viewport.setLocation(x, y);
		viewport.setSize(width, height);
		updateViewableMap();
	}
	
	/**
	 * Sets the visible area of this MapViewer instance in tiles.
	 * @param x the x-position of the visible area on the shown map
	 * @param y the y-position of the visible area on the shown map
	 * @param width the width of the visible area
	 * @param height the height of the visible area
	 */
	public void setViewportInTiles(int x, int y, int width, int height)
	{
		setViewportInPixels(x * tileWidth, y * tileHeight, width * tileWidth, height * tileHeight);
	}
	
	/**
	 * Sets the location of the visible area of this MapViewer instance
	 * in pixels.
	 * @param x the x-position of the visible area on the shown map
	 * @param y the y-position of the visible area on the shown map
	 */
	public void setViewportLocationInPixels(int x, int y)
	{
		viewport.setLocation(x, y);
		updateViewableMap();
	}
	
	/**
	 * Sets the location of the visible area of this MapViewer instance
	 * in tiles.
	 * @param x the x-position of the visible area on the shown map
	 * @param y the y-position of the visible area on the shown map
	 */
	public void setViewportLocationInTiles(int x, int y)
	{
		setViewportLocationInPixels(x * tileWidth, y * tileHeight);
	}

	/**
	 * Draws the visible area of this MapViewer instance in the top left corner.
	 */
	public void draw()
	{
		draw(0.f, 0.f);
	}
	
	/**
	 * Draws the visible area of this MapViewer instance at the given position.
	 * @param x the x-position
	 * @param y the y-position
	 */
	@Override
	public void draw(float x, float y)
	{
		if(viewableMap != null) {
			int imageOffsetLeft = 0;
			int imageOffsetTop = 0;
			int imageWidth = 0;
			int imageHeight = 0;
			float nextXPos = 0.f;
			float nextYPos = 0.f;
			for(int layer = 0, layerEnd = viewableMap.getLayerCount(); layer < layerEnd; ++layer) { 
				for(int column = 0, columnEnd = viewableMap.getWidth(); column < columnEnd; ++column) {
					nextXPos = column * tileWidth - (column == 0 && offsetLeft >= 0 ? 0 : offsetLeft);
					imageOffsetLeft = (column == 0 && offsetLeft >= 0 ? offsetLeft : 0);
					if(column == 0 && columnEnd > 1) {
						imageWidth = tileWidth - (offsetLeft >= 0 ? offsetLeft : 0);
					} else if(column == columnEnd - 1) {
						imageWidth = tileWidth - offsetRight - imageOffsetLeft;
					} else {
						imageWidth = tileWidth;
					}
					for(int row = 0, rowEnd = viewableMap.getHeight(); row < rowEnd; ++row) {
						nextYPos = row * tileHeight - (row == 0 && offsetTop >= 0 ? 0 : offsetTop);
						imageOffsetTop = (row == 0 && offsetTop >= 0 ? offsetTop : 0);
						if(row == 0 && rowEnd > 1) {
							imageHeight = tileHeight - (offsetTop >= 0 ? offsetTop : 0);
						} else if(row == rowEnd - 1) {
							imageHeight = tileHeight - offsetBottom - imageOffsetTop;
						} else {
							imageHeight = tileHeight;
						}
						viewableMap.getTile(column, row, layer).draw(x + nextXPos, y + nextYPos, imageOffsetLeft, imageOffsetTop, imageWidth, imageHeight);
					}
				}
			}
		}
	}
	
	/**
	 * Determines all of the maptiles that belong to the visible
	 * area of this MapViewer instance.
	 */
	private void updateViewableMap()
	{
		// Compute the x- and y-coordinate of the visible area depending on the map.
		// These coordinates can be negative if the visible area is to the left of or about
		// the map.
		int tileX = viewport.getX() / tileWidth;
		int tileY = viewport.getY() / tileHeight;
		// If the x- and / or y-coordinate is negative, then these coordinates need to be
		// adjusted depending on where the visible area is.
		if(viewport.getX() < 0 && viewport.getX() <= -(viewport.getWidth() % tileWidth) + (tileX * tileWidth)) {
			--tileX;
		}
		if(viewport.getY() < 0 && viewport.getY() <= -(viewport.getHeight() % tileHeight) + (tileY * tileHeight)) {
			--tileY;
		}
		// Determine how many tiles the visible area shows.
		int tileW = (viewport.getWidth() / tileWidth) + 1;
		int tileH = (viewport.getHeight() / tileHeight) + 1;
		// These values need to be adjusted if the visible area shows just
		// a portion of a tile.
		if(viewport.getX() + viewport.getWidth() > (tileX + tileW) * tileWidth) {
			++tileW;
		}
		if(viewport.getY() + viewport.getHeight() > (tileY + tileH) * tileHeight) {
			++tileH;
		}
		// If one of these conditions is met, then there is no tile inside
		// the visible area of this MapViewer instance.
		if(tileX >= map.getWidth() || tileY >= map.getHeight() || tileW + tileX <= 0 || tileH + tileY <= 0) {
			offsetLeft = offsetTop = offsetRight = offsetBottom = 0;
			viewableMap = null;
		} else {
			// If the visible area is to the left of or about the map then
			// the number of tiles the visible area shows needs to be corrected.
			if(tileX < 0) {
				tileW += tileX;
				tileX = 0;
			}
			if(tileY < 0) {
				tileH += tileY;
				tileY = 0;
			}
			// Compute the margin of the images of the tiles in the first row / column.
			offsetLeft = viewport.getX() - (tileX * tileWidth);
			offsetTop = viewport.getY() - (tileY * tileHeight);
			
			// Compute the margin of the images of the tiles in the last row / column.
			if(tileX + tileW > map.getWidth()) {
				tileW = map.getWidth() - tileX;
				offsetRight = 0;
			} else {
				offsetRight = ((tileX + tileW) * tileWidth) - (viewport.getX() + viewport.getWidth());
			}
			if(tileY + tileH > map.getHeight()) {
				tileH = map.getHeight() - tileY;
				offsetBottom = 0;
			} else {
				offsetBottom = ((tileY + tileH) * tileHeight) - (viewport.getY() + viewport.getHeight());
			}
			// Finally get a copy of all the tiles inside the visible area.
			viewableMap = map.getMapExcerpt(tileX, tileY, tileW, tileH);
		}
	}
}
